package jmine.tec.report.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.Transient;

import jmine.tec.di.annotation.InjectedBean;
import jmine.tec.di.annotation.InjectedProperty;
import jmine.tec.report.ColumnFactory;
import jmine.tec.report.CurriedDataColumn;
import jmine.tec.report.DataColumn;
import jmine.tec.report.ExecutableDataColumn;
import jmine.tec.report.ExecutableDataColumnContainer;
import jmine.tec.report.ExecutableDataTable;
import jmine.tec.report.TableDescriptor;
import jmine.tec.report.impl.SimpleCurriedDataColumn;
import jmine.tec.report.impl.SimpleDataColumn;
import jmine.tec.report.impl.SimpleExecutableDataTable;
import jmine.tec.report.metadata.ColumnMetaData;
import jmine.tec.report.metadata.ParameterDefinition;
import jmine.tec.report.metadata.ParameterType;
import jmine.tec.report.metadata.impl.UnmodifiableColumnMetaData;

import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.audit.Auditable;
import bancosys.tec.persist.authorization.Authorizable;
import bancosys.tec.persist.bussobj.AuthPersistableBusinessObject;

/**
 * Implementacao de {@link TableDescriptor} que eh persistivel
 * 
 * @author takeshi
 */
@Entity
@InjectedBean(useBeanFactoryDefinition = false)
public class PersistableTableDescriptor extends AuthPersistableBusinessObject implements TableDescriptor, Persistable, Authorizable,
        Auditable {

    private Map<String, Object> parameterValues;

    private List<PersistableDataColumn> columnDefinitions = new ArrayList<PersistableDataColumn>();

    private List<DataColumn> simpleColumns;

    private Map<String, ParameterDefinition> mergedParameters;

    private Long id;

    private ExecutableDataColumnContainer container;

    private List<PersistableTuple> parameters = new ArrayList<PersistableTuple>();

    /**
     * C`tor
     */
    public PersistableTableDescriptor() {
    }

    /**
     * copy c'tor
     * 
     * @param other o outro table descritptor
     */
    public PersistableTableDescriptor(TableDescriptor other) {
        this.simpleColumns = new ArrayList<DataColumn>();
        for (DataColumn col : other.getColumns()) {
            if (col instanceof CurriedDataColumn) {
                CurriedDataColumn curried = (CurriedDataColumn) col;
                this.addColumnCurry(curried.getMetaData(), curried.getAllCurriedParameters());
            } else {
                this.addColumn(col.getMetaData());
            }
        }
        this.mergedParameters = new HashMap<String, ParameterDefinition>();
        this.mergedParameters.putAll(other.getMergedParameterDefinition());
    }

    /**
     * {@inheritDoc}
     */
    public DataColumn addColumn(ColumnMetaData column) {
        SimpleDataColumn simple = new SimpleDataColumn(column);
        this.getColumns().add(simple);
        this.columnDefinitions.add(new PersistableDataColumn(column.getKey(), this));
        return simple;
    }

    /**
     * {@inheritDoc}
     */
    public DataColumn addColumnCurry(ColumnMetaData meta, Map<String, Object> paramsToCurry) {
        SimpleCurriedDataColumn column = new SimpleCurriedDataColumn(new SimpleDataColumn(meta), paramsToCurry);
        this.getColumns().add(column);
        this.columnDefinitions.add(new PersistableCurriedDataColumn(meta.getKey(), this, paramsToCurry));
        return column;
    }

    /**
     * {@inheritDoc}
     */
    public void addColumnFactory(ColumnFactory factory, ColumnMetaData meta) {
        throw new UnsupportedOperationException("cannot add arbitrary column factories");
    }

    /**
     * {@inheritDoc}
     */
    public void revalidateParameters() {
        this.mergedParameters = new HashMap<String, ParameterDefinition>();
        for (final DataColumn col : this.getColumns()) {
            this.mergeParameters(col.getMetaData().getParameters());
        }
    }

    /**
     * @param values the values
     */
    private void mergeParameters(final Collection<ParameterDefinition> values) {
        for (final ParameterDefinition parameterDefinition : values) {
            this.mergedParameters.put(parameterDefinition.getName(), parameterDefinition);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void addColumnFactoryCurry(ColumnFactory factory, ColumnMetaData meta, Map<String, Object> paramsToCurry) {
        throw new UnsupportedOperationException("cannot add arbitrary column factories");
    }

    /**
     * {@inheritDoc}
     */
    @Transient
    public List<DataColumn> getColumns() {
        if (this.simpleColumns == null) {

            this.buildColumnsRevalidateParameters();
        }
        return this.simpleColumns;
    }

    /**
     */
    private void buildColumnsRevalidateParameters() {
        this.mergedParameters = new HashMap<String, ParameterDefinition>();
        if (this.getColumnDefinitions().isEmpty()) {
            this.simpleColumns = new ArrayList<DataColumn>();
            return;
        }
        this.simpleColumns = new ArrayList<DataColumn>(this.columnDefinitions.size());
        if (this.container != null) {
            Collections.sort(this.getColumnDefinitions());
            for (PersistableDataColumn col : this.getColumnDefinitions()) {
                ExecutableDataColumn executable = col.buildColumns(this.container).get(0);
                ColumnMetaData meta = executable.getMetaData();

                DataColumn column = null;
                if (col instanceof PersistableCurriedDataColumn) {
                    PersistableCurriedDataColumn curried = (PersistableCurriedDataColumn) col;
                    column = new SimpleCurriedDataColumn(new SimpleDataColumn(meta), curried.getParameters());
                } else {
                    column = new SimpleDataColumn(new UnmodifiableColumnMetaData(meta));
                }

                this.simpleColumns.add(column);
                this.mergeParameters(column.getMetaData().getParameters());
            }
        } else {
            throw new IllegalStateException("Cannot create columns without the column container");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Transient
    public Map<String, ParameterDefinition> getMergedParameterDefinition() {
        if (this.mergedParameters == null) {
            this.revalidateParameters();
        }
        return this.mergedParameters;
    }

    /**
     * {@inheritDoc}
     */
    public ParameterType getParameterType(String paramName) {
        ParameterDefinition definition = this.getMergedParameterDefinition().get(paramName);
        return definition == null ? null : definition.getType();
    }

    /**
     * {@inheritDoc}
     */
    public void setParameterValue(String param, Object obj) {
        if (this.getParameterValues().containsKey(param)) {
            Iterator<PersistableTuple> iterator = this.getParameters().iterator();
            while (iterator.hasNext()) {
                if (iterator.next().head().equals(param)) {
                    iterator.remove();
                }
            }
        }
        this.getParameters().add(new PersistableTuple(param, obj));
        this.getParameterValues().put(param, obj);
    }

    /**
     * @return the id
     */
    @Id
    @GeneratedValue
    public Long getId() {
        return this.id;
    }

    /**
     * @param id the id to set
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Serializable getPk() {
        return this.getId();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setPk(Serializable pk) {
        this.setId((Long) pk);
    }

    /**
     * @return the parameterValues
     */
    @Transient
    public Map<String, Object> getParameterValues() {
        if (this.parameterValues == null) {
            this.parameterValues = new HashMap<String, Object>(this.getParameters().size());
            for (PersistableTuple tuple : this.getParameters()) {
                this.parameterValues.put(tuple.head(), tuple.tail());
            }
        }
        return this.parameterValues;
    }

    /**
     * {@inheritDoc}
     */
    public final DataColumn addColumn(String key, String name, ParameterDefinition... params) {
        return this.addColumn(new UnmodifiableColumnMetaData(key, name, params));
    }

    /**
     * {@inheritDoc}
     */
    public final DataColumn addColumnCurry(String key, String name, Map<String, Object> paramsToCurry, ParameterDefinition... params) {
        return this.addColumnCurry(new UnmodifiableColumnMetaData(key, name, params), paramsToCurry);
    }

    /**
     * {@inheritDoc}
     */
    @Transient
    public final int getNumberOfColumn() {
        return this.getColumns().size();
    }

    /**
     * {@inheritDoc}
     */
    public final DataColumn getColumn(int index) {
        return this.getColumns().get(index);
    }

    /**
     * {@inheritDoc}
     */
    public final void removeColumnWithIndex(int index) {
        this.columnDefinitions.remove(index);
        this.getColumns().remove(index);
        this.revalidateParameters();
    }

    /**
     * Creates an {@link ExecutableDataTable} from the added columns and the given {@link ExecutableDataColumnContainer}
     * 
     * @param externalContainer the container
     * @return {@link ExecutableDataTable}
     */
    public final ExecutableDataTable buildExecutableDataTable(final ExecutableDataColumnContainer externalContainer) {
        final SimpleExecutableDataTable table = new SimpleExecutableDataTable();
        for (final DataColumn col : this.getColumns()) {
            if (col instanceof ColumnFactory) {
                final ColumnFactory factory = (ColumnFactory) col;
                List<ExecutableDataColumn> factoryColumns = factory.buildColumns(externalContainer);
                for (ExecutableDataColumn exeCol : factoryColumns) {
                    if (exeCol == null) {
                        throw new IllegalArgumentException("Column factory returned a null column: " + col.getKey());
                    }
                    table.addColumn(exeCol);
                }
            } else {
                final ExecutableDataColumn exeCol = externalContainer.findExecutableDataColumn(col.getKey());
                if (exeCol == null) {
                    throw new IllegalArgumentException("Cannot find column with key: " + col.getKey());
                }
                table.addColumn(exeCol);
            }
        }
        return table;
    }

    /**
     * {@inheritDoc}
     */
    public Object getParam(String param) {
        return this.getParameterValues().get(param);
    }

    /**
     * @return the columnDefinitions
     */
    @OneToMany(mappedBy = "owner")
    @Cascade({ CascadeType.ALL, CascadeType.DELETE_ORPHAN })
    public List<PersistableDataColumn> getColumnDefinitions() {
        return this.columnDefinitions;
    }

    /**
     * @param columnDefinitions the columnDefinitions to set
     */
    public void setColumnDefinitions(List<PersistableDataColumn> columnDefinitions) {
        this.columnDefinitions = columnDefinitions;
    }

    /**
     * @param container the container to set
     */
    @InjectedProperty(required = true, beanName = "reportManager")
    public void setContainer(ExecutableDataColumnContainer container) {
        this.container = container;
    }

    /**
     * @return the parameters
     */
    @OneToMany
    @JoinTable(name = "TBL_DESC_PARAMS")
    @Cascade({ CascadeType.ALL, CascadeType.DELETE_ORPHAN })
    public List<PersistableTuple> getParameters() {
        return this.parameters;
    }

    /**
     * @param parameters the parameters to set
     */
    public void setParameters(List<PersistableTuple> parameters) {
        this.parameters = parameters;
    }

}
